CodeBuild用のLambda実行環境に乗り込んで中身を色々分析してみた
CX事業部@大阪の岩田です。
先日のアップデートでCodeBuildの実行環境としてLambdaが選択可能になりました
このCodeBuildで利用するLambda実行環境が通常のLambda実行環境とどのように違うのか気になったので、CodeBuildのジョブを実行しているLambda実行環境に乗り込んで色々確認してみることにしました。
Lambda実行環境に乗り込む準備
ソースコードの準備
以前ブログで紹介したserverless-preyを使うとLambda実行環境に乗り込んでシェルを実行できます。
今回は通常のLambdaの利用ではなく、CodeBuildのジョブを実行しているLambda実行環境に乗り込んでシェルを実行したいので、serverless-preyのソースコードを参考に、ngrok経由でローカルマシンのターミナルに接続する処理を書きます。
参考にしたソースコードはこちら https://github.com/pumasecurity/serverless-prey/blob/e764f6e10aadad2f1ff4aae684103432fb288a01/panther/src/panther/handler.js
const net = require("net") const cp = require("child_process") const host = process.env.NGROK_HOST const portNum = parseInt(process.env.NGROK_PORT) const sh = cp.spawn("/bin/sh", []) const client = new net.Socket() const main = async () => { await new Promise((resolve, reject) => { client.connect(portNum, host, () => { client.pipe(sh.stdin) sh.stdout.pipe(client) sh.stderr.pipe(client) }) client.on("close", (hadError) => { if (hadError) { reject(new Error("Transmission error.")); } else { resolve() } }) client.on("end", () => { console.log(("shhutdown")) resolve() }) client.on("error", (err) => { console.error(err) reject(err) }) client.on("timeout", () => { console.error("timeout") reject(new Error("Socket timeout.")) }) }) } main()
このコードを実行するためにbuildspec.ymlを記述します
version: 0.2 phases: build: commands: - node shell.js
コードの準備ができたので、これら2つのファイルをZIPに圧縮して適当なS3バケットにアップします。
実際にLambda実行環境に乗り込んでみる
Lambda実行環境と接続するローカル環境のターミナルを準備します
nc -l 4444
上記のポートにインターネット経由でアクセスできるようngrokを起動します
ngrok tcp 4444
以下のように表示されるので、ngrokのエンドポイントとポート番号を確認します。
ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Account <ngrokのアカウント名> (Plan: Free) Update update available (version 2.3.41, Ctrl-U to update) Version 2.3.40 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding tcp://<ngrokのホスト>:<ngrokのポート番号> -> localhost:4444 Connections ttl opn rt1 rt5 p50 p90 0 0 0.00 0.00 0.00 0.00
先程用意したZIPファイルを元にビルドジョブを実行するCodeBuildのプロジェクトを作成します。今回はNode.js 18のLambda実行環境でビルドジョブを実行するよう設定しました。
プロジェクトの準備ができたら「上書きでビルドを開始する」を選択し、環境変数を以下のように設定します
- NGROK_HOST: 先程確認したngrokのホスト
- NGROK_PORT: 先程確認したngrokのポート番号
ビルドを開始後するとncを実行していたローカル環境のターミナルがLambda実行環境と接続され、シェルが叩けるようになります。
とりあえずwhoami
でも叩いてみましょう
sbx_user1051
uname -a Linux 169.254.19.173 4.14.255-327-266.539.amzn2.x86_64 #1 SMP Sun Oct 22 13:13:51 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
これで準備OKです!
通常のLambda実行環境との差分を確認する
ここからは実際に色々とコマンドを叩いて出力を確認していき通常のLambda実行環境との差異を確認していきます。ランタイムは通常のLambda、CodeBuildともにx86_64のNode.js 18xとしました。
全般
まずはls /
から
bin boot codebuild dev etc home lambda-entrypoint.sh lib lib64 main media mnt opt proc root run sbin srv sys THIRD-PARTY-LICENSES.txt tmp usr var
codebuild
というディレクトリが特徴的です。
続いてls /home
codebuild-user
codebuild-userなるユーザーが存在するようです。ls /home/codebuild-user
はどうでしょう?
ls: cannot open directory /home/codebuild-user: Permission denied
見えませんでした。残念。
ls /tmp
で/tmpの中身も見てみましょう
agent agent-log codebuild git-credential-helper mcetmp691865295
なるほど。通常のLambda実行環境とは色々異なりますね。
/tmp/agentについてfile /tmp/agent
でもう少し詳細を見ておきましょう。
/tmp/agent: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
/bin/と/sbinの中身を確認してみましょう。
ls /bin | wc -c
7433
お?通常のLambda実行環境だと結果は1035だったので、使えるコマンドが増えていそうです。
/sbin配下はどうでしょう?
ls /sbin | wc -c
2019
こちらも通常のLambda実行環境だと結果は231だったので、通常のLambda実行環境よりも多くのコマンドが利用できるようです。少し触ってみましたが、psなども使えるので、環境調査が捗りそうです。
Lambda実行環境の裏側が垣間見えるfindmnt
を実行してみましょう
TARGET SOURCE FSTYPE OPTIONS / /mnt/root-rw/opt/amazon/asc/worker/tasks/rtfs/inline-manifest overlay ro,nosuid,nodev,relatime,lowerdir=/tmp/es3646364495/8c01de1164174241:/tmp/es3646364495/34f08a2802910111 ├─/dev /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/sandbox-dev] ext4 rw,nosuid,noexec,noatime,data=writeback ├─/tmp /dev/vdd ext4 rw,relatime,data=writeback ├─/proc none proc rw,nosuid,nodev,noexec,noatime │ └─/proc/sys/kernel/random/boot_id /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/boot_id-IMZGEN] ext4 ro,nosuid,nodev,noatime,data=writeback ├─/etc/passwd /dev/root[/etc/passwd] ext4 ro,nosuid,nodev,relatime,data=ordered ├─/var/rapid /dev/root[/opt/amazon/asc/worker/runtime/byol] ext4 ro,nosuid,nodev,relatime,data=ordered └─/etc/resolv.conf /dev/vdb[/opt/amazon/asc/worker/sandbox-tmp/sbtasks/resolv.confSANDBOX] ext4 ro,nosuid,nodev,noatime,data=writeback
色々と妄想が膨らみますね。
最後にdf -h
の結果です
Filesystem Size Used Avail Use% Mounted on /mnt/root-rw/opt/amazon/asc/worker/tasks/rtfs/inline-manifest 1.5G 9.4M 1.4G 1% / /dev/vdb 1.5G 9.4M 1.4G 1% /dev /dev/vdd 11G 65M 10G 1% /tmp /dev/root 9.7G 552M 9.2G 6% /etc/passwd
カーネルバージョンなど
続いてカーネルの情報を確認します。まずはuname -a
uname -a Linux 169.254.19.173 4.14.255-327-266.539.amzn2.x86_64 #1 SMP Sun Oct 22 13:13:51 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux
カーネルバージョン4.14.xのAmazon Linux2のようです。ちなみに通常のLambda実行環境だとカーネルバージョン5.10.xのAmazon Linuxが利用されています。
続いてcat /etc/os-release
NAME="Amazon Linux" VERSION="2" ID="amzn" ID_LIKE="centos rhel fedora" VERSION_ID="2" PRETTY_NAME="Amazon Linux 2" ANSI_COLOR="0;33" CPE_NAME="cpe:2.3:o:amazon:amazon_linux:2" HOME_URL="https://amazonlinux.com/" SUPPORT_END="2025-06-30" VARIANT_ID="202310201457-2.0.1141.0"
通常のLambda実行環境には存在しないSUPPORT_ENDという項目が増えています。結果を比較すると以下のようになりました。
確認した項目 | 通常のLambda実行環境 | CodeBuild用のLambda実行環境 |
---|---|---|
カーネルバージョン | 5.10.196-205.748.amzn2.x86_64 | 4.14.255-327-266.539.amzn2.x86_64 |
/etc/os-releaseのVARIANT_ID | 202308280853-2.0.1118.0 | 202310201457-2.0.1141.0 |
環境変数
次は環境変数を確認します。env | sort
の実行結果を比較すると以下のような違いがありました
環境変数名 | 通常のLambda実行環境 | CodeBuild用のLambda実行環境 |
---|---|---|
AWS_LAMBDA_FUNCTION_NAME | 対象のLambda関数の名前 | customer-<AWSアカウントID>-<ランダムな文字列> 例)customer-<AWSアカウントID>-dfcf259c-b421-47d7-ac49-645ce9bb7e3a-5aaVd |
AWS_LAMBDA_LOG_GROUP_NAME | CW Logsのロググループ名 | 存在せず |
AWS_LAMBDA_LOG_STREAM_NAME | CW Logsのログストリーム名 | 存在せず |
_HANDLER | Lambdaの設定で指定したhandler | 空 |
PATH | /var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin | /tmp/opt/npm/bin:/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin:/tmp/codebuild/bin:/codebuild/user/bin |
PWD | /var/task | /tmp/codebuild/outputで始まるディレクトリ 例)/tmp/codebuild/output/src3154/src/s3/01 |
SHLVL | 1 | 4 |
TZ | :UTC | :/etc/localtime |
AWS_LAMBDA_FUNCTION_NAMEあたりが面白いですね。PATHもかなり差異があります。
また、以下の環境変数についてはCodeBuild用のLambda実行環境にのみ存在しました
環境変数名 | 値 |
---|---|
HOME | /tmp |
LAMBDA_USER_HOME | /tmp/opt |
MAVEN_OPTS | -Dmaven.wagon.httpconnectionManager.maxPerRoute=2 |
CODEBUILD_ACTION_RUNNER_URL | https://codefactory-ap-northeast-1-prod-default-build-agent-executor.s3.ap-northeast-1.amazonaws.com/cawsrunner.zip |
CODEBUILD_AUTH_TOKEN | UUID4の文字列 |
CODEBUILD_BUILD_ARN | 対象ビルドジョブのARN |
CODEBUILD_BUILD_ID | 対象ビルドジョブのID |
CODEBUILD_BUILD_IMAGE | aws/codebuild/amazonlinux-x86_64-lambda-standard:nodejs18 |
CODEBUILD_BUILD_NUMBER | 対象のビルドNO |
CODEBUILD_BUILD_SUCCEEDING | 1 |
CODEBUILD_BUILD_URL | 対象ビルドの詳細を確認するためのマネコンのURL |
CODEBUILD_EXECUTOR_AGENT_TYPE | CBMCE |
CODEBUILD_FE_REPORT_ENDPOINT | https://codebuild.<リージョン>.amazonaws.com |
CODEBUILD_GOPATH | /tmp/codebuild/output/から始まるディレクトリ 例/tmp/codebuild/output/src3154 |
CODEBUILD_INITIATOR | ビルドに使用したIAMロール名 |
CODEBUILD_KMS_KEY_ID | ビルドに使用したKMSのキーID |
CODEBUILD_LAST_EXIT | 前回のビルド結果?? 0が設定されていました |
CODEBUILD_LOG_PATH | UUID4の文字列 |
CODEBUILD_PROJECT_UUID | UUID4の文字列 |
CODEBUILD_SOURCE_REPO_URL | ビルド対象のURL |
CODEBUILD_SRC_DIR | /tmp/codebuild/output/から始まるディレクトリ 例)/tmp/codebuild/output/src3154/src/s3/01 |
CODEBUILD_START_TIME | ビルドが開始されたタイムスタンプ |
GOPATH | /tmp/codebuild/output/から始まるディレクトリ 例)/tmp/codebuild/output/src3154 |
注目したいのはCODEBUILD_ACTION_RUNNER_URLに設定されたURLです。ここからDLしたZIPファイルを分析すると色々面白いかも・・・?
その他CODEBUILD_から始まる環境変数の意味については以下のリンクも参考になります
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html
起動しているプロセスなど
起動しているプロセスなど確認してみましょう。まずはps aux
の結果から
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND sbx_use+ 1 0.0 0.4 1238112 5904 ? Ssl 01:11 0:00 /var/rapid/init --logs-egress-api fluxpump --oci-config - --enable-extensions sbx_use+ 8 0.0 0.4 715600 5244 ? Sl 01:11 0:00 /main sbx_use+ 28 0.5 2.2 1250980 27852 ? Sl 01:11 0:00 ./agent sbx_use+ 34 0.0 0.2 121992 2776 ? S 01:11 0:00 /bin/sh /tmp/mcetmp691865295/script.sh sbx_use+ 35 0.0 1.1 1241600 13700 ? Sl 01:11 0:00 ./executor sbx_use+ 50 0.0 0.2 121992 2836 ? S 01:11 0:00 /bin/sh /tmp/codebuild/output/tmp/script.sh sbx_use+ 52 0.1 3.5 723628 43716 ? Sl 01:11 0:00 node shell.js sbx_use+ 59 0.0 0.2 121992 2780 ? S 01:11 0:00 /bin/sh sbx_use+ 67 0.0 0.3 160212 3760 ? R 01:12 0:00 ps aux
ビルドジョブから起動したプロセス以外に以下のプロセスが動いています。この辺は通常のLambdaと結構違いますね
/var/rapid/init --logs-egress-api fluxpump --oci-config - --enable-extensions
/main
./agent
/bin/sh /tmp/mcetmp691865295/script.sh
./executor
/bin/sh /tmp/codebuild/output/tmp/script.sh
このうちPID 1の/var/rapid/init
は通常のLambda Functionと同様ですが、微妙に引数が異なります。通常のLambda Functionであれば/var/rapid/init --enable-extensions --bootstrap=/var/runtime/bootstrap --logs-egress-api=fluxpump --enable-msg-logs
なので、CodeBuild用のLambda実行環境では引数の--bootstrap
と--enable-msg-logs
が付与されていないことになります。
PID8の/main
に関してですが、file /main
を確認したところ、以下のような出力でした。
/main: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
LambdaのランタイムAPIを叩いてみる
CodeBuildのジョブは通常のLambdaとは異なり、イベント発生の都度handlerが起動して...といった処理は行わないはずですが、LambdaのランタイムAPIは通常通り利用できるのでしょうか?curlコマンドが使えたので、以下のリンクを参考にLambdaのランタイムAPIを叩いてみました。
https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html#runtimes-api-response
まずはイベントデータを取得するためのNext invocationから
curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next" -s | jq .
レスポンスです
{ "agentBinaryPrefix": "s3://mce-prod-ap-northeast-1-11-mce-agent-binary/bin/linux_amd64/", "borderServiceEndpoint": "https://border-11.ap-northeast-1.prod.compute.caws.dev-tools.aws.dev:443/agent/", "computeTaskArn": "arn:aws:mce:ap-northeast-1:<AWSアカウントID>:compute-task/<UUID4の文字列>", "computeTaskToken": "<UUID4の文字列>", "fleetType": "LAMBDA_MULTI_TENANT", "region": "ap-northeast-1", "rootDir": "/codebuild", "logStreamName": "<AWSアカウントID>/58f8b8c5-d139-420e-a6be-d228a6d65826", "logGroupName": "mce-prod-ap-northeast-1-11-agent-logs" }
中々興味深い内容です。このイベントデータを元にしてビルドジョブを実行しているとかでしょうかね?
続いてInvocation response
REQUEST_ID=156cb537-e2d4-11e8-9b34-d36013741fb9 curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -s -d "SUCCESS" | jq .
レスポンスです。
{ "errorMessage": "Invalid request ID", "errorType": "InvalidRequestID" }
妥当なリクエストIDが分からないので、リクエストを成功させることは難しそうですねぇ...
次はInitialization error
ERROR="{\"errorMessage\" : \"Failed to load function.\", \"errorType\" : \"InvalidFunctionException\"}" curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/init/error" -d "$ERROR" --header "Lambda-Runtime-Function-Error-Type: Unhandled" -s | jq .
レスポンスです。
{ "errorMessage": "State transition from Running to InitError failed for runtime. Error: State transition is not allowed", "errorType": "InvalidStateTransition" }
Initialization処理が完了しているのにInitialization errorのAPIを叩いているのでエラーになります。まあ当然ですよね。
最後にInvocation error
REQUEST_ID=156cb537-e2d4-11e8-9b34-d36013741fb9 ERROR="{\"errorMessage\" : \"Error parsing event data.\", \"errorType\" : \"InvalidEventDataException\"}" curl "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/error" -d "$ERROR" --header "Lambda-Runtime-Function-Error-Type: Unhandled" -s | jq .
レスポンスです。
{ "errorMessage": "Invalid request ID", "errorType": "InvalidRequestID" }
Invocation responseと同様にリクエストを成功させるのは難しそうです。
まとめ
同じLambda実行環境と言っても中身は色々と違いがあることが分かりました。CodeBuildの裏側ではどのようにLambdaと連携しているるのでしょうか?この辺もre:invent2023のセッションで詳細が聞けると楽しそうですね。